// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:js_shared/variance.dart';
import 'package:kernel/ast.dart' as ir;

import '../closure.dart';
import '../common.dart';
import '../elements/entities.dart';
import '../elements/names.dart';
import '../elements/types.dart';
import '../ir/closure.dart';
import '../ir/element_map.dart';
import '../js_backend/annotations.dart';
import '../js_model/element_map.dart';
import '../ordered_typeset.dart';
import '../serialization/deferrable.dart';
import '../serialization/serialization.dart';
import '../universe/selector.dart';
import 'class_type_variable_access.dart';
import 'elements.dart';
import 'env.dart';
import 'js_world_builder.dart' show JClosedWorldBuilder;

class ClosureDataImpl implements ClosureData {
  /// Tag used for identifying serialized [ClosureData] objects in a
  /// debugging data stream.
  static const String tag = 'closure-data';

  final JsToElementMap _elementMap;

  /// Map of the scoping information that corresponds to a particular entity.
  final Map<MemberEntity, ScopeInfo> _scopeMap;
  final Deferrable<Map<ir.TreeNode, CapturedScope>> _capturedScopesMap;
  // Indicates the type variables (if any) that are captured in a given
  // Signature function.
  final Deferrable<Map<MemberEntity, CapturedScope>>
  _capturedScopeForSignatureMap;

  final Deferrable<Map<ir.LocalFunction, ClosureRepresentationInfo>>
  _localClosureRepresentationMap;

  final Map<MemberEntity, MemberEntity> _enclosingMembers;

  ClosureDataImpl(
    this._elementMap,
    this._scopeMap,
    Map<ir.TreeNode, CapturedScope> capturedScopesMap,
    Map<MemberEntity, CapturedScope> capturedScopeForSignatureMap,
    Map<ir.LocalFunction, ClosureRepresentationInfo>
    localClosureRepresentationMap,
    this._enclosingMembers,
  ) : _capturedScopesMap = Deferrable.eager(capturedScopesMap),
      _capturedScopeForSignatureMap = Deferrable.eager(
        capturedScopeForSignatureMap,
      ),
      _localClosureRepresentationMap = Deferrable.eager(
        localClosureRepresentationMap,
      );

  ClosureDataImpl._deserialized(
    this._elementMap,
    this._scopeMap,
    this._capturedScopesMap,
    this._capturedScopeForSignatureMap,
    this._localClosureRepresentationMap,
    this._enclosingMembers,
  );

  static Map<ir.TreeNode, CapturedScope> _readCapturedScopesMap(
    DataSourceReader source,
  ) {
    return source.readTreeNodeMap(
      () => CapturedScope.readFromDataSource(source),
    );
  }

  static Map<MemberEntity, CapturedScope> _readCapturedScopeForSignatureMap(
    DataSourceReader source,
  ) {
    return source.readMemberMap(
      (_) => CapturedScope.readFromDataSource(source),
    );
  }

  static Map<ir.LocalFunction, ClosureRepresentationInfo>
  _readLocalClosureRepresentationMap(DataSourceReader source) {
    return source.readTreeNodeMap<ir.LocalFunction, ClosureRepresentationInfo>(
      () => ClosureRepresentationInfo.readFromDataSource(source),
    );
  }

  /// Deserializes a [ClosureData] object from [source].
  factory ClosureDataImpl.readFromDataSource(
    JsToElementMap elementMap,
    DataSourceReader source,
  ) {
    source.begin(tag);
    // TODO(johnniwinther): Support shared [ScopeInfo].
    final scopeMap = source.readMemberMap(
      (MemberEntity member) =>
          source.readDeferrable(ScopeInfo.readFromDataSource),
    );
    final capturedScopesMap = source.readDeferrable(_readCapturedScopesMap);
    final capturedScopeForSignatureMap = source.readDeferrable(
      _readCapturedScopeForSignatureMap,
    );
    final localClosureRepresentationMap = source.readDeferrable(
      _readLocalClosureRepresentationMap,
    );
    Map<MemberEntity, MemberEntity> enclosingMembers = source.readMemberMap(
      (member) => source.readMember(),
    );
    source.end(tag);
    return ClosureDataImpl._deserialized(
      elementMap,
      DeferrableValueMap(scopeMap),
      capturedScopesMap,
      capturedScopeForSignatureMap,
      localClosureRepresentationMap,
      enclosingMembers,
    );
  }

  /// Serializes this [ClosureData] to [sink].
  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.begin(tag);
    sink.writeMemberMap(
      _scopeMap,
      (_, ScopeInfo info) =>
          sink.writeDeferrable(() => info.writeToDataSink(sink)),
    );
    sink.writeDeferrable(
      () => sink.writeTreeNodeMap(_capturedScopesMap.loaded(), (
        CapturedScope scope,
      ) {
        scope.writeToDataSink(sink);
      }),
    );
    sink.writeDeferrable(
      () => sink.writeMemberMap(
        _capturedScopeForSignatureMap.loaded(),
        (MemberEntity member, CapturedScope scope) =>
            scope.writeToDataSink(sink),
      ),
    );
    sink.writeDeferrable(
      () => sink.writeTreeNodeMap(_localClosureRepresentationMap.loaded(), (
        ClosureRepresentationInfo info,
      ) {
        info.writeToDataSink(sink);
      }),
    );
    sink.writeMemberMap(_enclosingMembers, (
      MemberEntity member,
      MemberEntity value,
    ) {
      sink.writeMember(value);
    });
    sink.end(tag);
  }

  @override
  ScopeInfo getScopeInfo(MemberEntity entity) {
    // TODO(johnniwinther): Remove this check when constructor bodies a created
    // eagerly with the J-model; a constructor body should have it's own
    // [ClosureRepresentationInfo].
    if (entity is ConstructorBodyEntity) {
      entity = entity.constructor;
    } else if (entity is JParameterStub) {
      entity = entity.target;
    }

    return _scopeMap[entity]!;
  }

  // TODO(efortuna): Eventually capturedScopesMap[node] should always
  // be non-null, and we should just test that with an assert.
  @override
  CapturedScope getCapturedScope(MemberEntity entity) {
    MemberDefinition definition = _elementMap.getMemberDefinition(entity);
    switch (definition.kind) {
      case MemberKind.regular:
      case MemberKind.constructor:
      case MemberKind.constructorBody:
      case MemberKind.closureCall:
        return _capturedScopesMap.loaded()[definition.node] ??
            const CapturedScope();
      case MemberKind.signature:
        return _capturedScopeForSignatureMap.loaded()[entity] ??
            const CapturedScope();
      case MemberKind.parameterStub:
        return const CapturedScope();
      case MemberKind.closureField:
      case MemberKind.generatorBody:
      case MemberKind.recordGetter:
        throw failedAt(entity, "Unexpected member definition $definition");
    }
  }

  @override
  CapturedScope getCapturedBlockScope(ir.Block blockNode) =>
      _capturedScopesMap.loaded()[blockNode] ?? const CapturedScope();

  @override
  // TODO(efortuna): Eventually capturedScopesMap[node] should always
  // be non-null, and we should just test that with an assert.
  CapturedLoopScope getCapturedLoopScope(ir.Node loopNode) =>
      _capturedScopesMap.loaded()[loopNode] as CapturedLoopScope? ??
      const CapturedLoopScope();

  @override
  ClosureRepresentationInfo getClosureInfo(ir.LocalFunction node) =>
      _localClosureRepresentationMap.loaded()[node]!;

  @override
  MemberEntity getEnclosingMember(MemberEntity member) {
    return _enclosingMembers[member] ?? member;
  }
}

/// Closure conversion code using our new Entity model. Closure conversion is
/// necessary because the semantics of closures are slightly different in Dart
/// than JavaScript. Closure conversion is separated out into two phases:
/// generation of a new (temporary) representation to store where variables need
/// to be hoisted/captured up at another level to re-write the closure, and then
/// the code generation phase where we generate elements and/or instructions to
/// represent this new code path.
///
/// For a general explanation of how closure conversion works at a high level,
/// check out:
/// http://siek.blogspot.com/2012/07/essence-of-closure-conversion.html or
/// http://matt.might.net/articles/closure-conversion/.

class ClosureDataBuilder {
  final DiagnosticReporter _reporter;
  final JsToElementMap _elementMap;
  final AnnotationsData _annotationsData;

  /// Map of the scoping information that corresponds to a particular entity.
  final Map<MemberEntity, ScopeInfo> _scopeMap = {};
  final Map<ir.TreeNode, CapturedScope> _capturedScopesMap = {};
  // Indicates the type variables (if any) that are captured in a given
  // Signature function.
  final Map<MemberEntity, CapturedScope> _capturedScopeForSignatureMap = {};

  final Map<ir.LocalFunction, ClosureRepresentationInfo>
  _localClosureRepresentationMap = {};

  final Map<MemberEntity, MemberEntity> _enclosingMembers = {};

  ClosureDataBuilder(this._reporter, this._elementMap, this._annotationsData);

  void _updateScopeBasedOnRtiNeed(
    KernelScopeInfo scope,
    ClosureRtiNeed rtiNeed,
    MemberEntity outermostEntity,
  ) {
    bool includeForRti(Set<VariableUse> useSet) {
      for (VariableUse usage in useSet) {
        switch (usage) {
          case SimpleVariableUse.explicit:
            return true;
          case SimpleVariableUse.implicitCast:
            if (_annotationsData
                .getImplicitDowncastCheckPolicy(outermostEntity)
                .isEmitted) {
              return true;
            }
            break;
          case SimpleVariableUse.localType:
            break;
          case ConstructorTypeArgumentVariableUse(:final member):
            ConstructorEntity constructor = _elementMap.getConstructor(member);
            if (rtiNeed.classNeedsTypeArguments(constructor.enclosingClass)) {
              return true;
            }
            break;
          case StaticTypeArgumentVariableUse(:final procedure):
            FunctionEntity method = _elementMap.getMethod(procedure);
            if (rtiNeed.methodNeedsTypeArguments(method)) {
              return true;
            }
            break;
          case InstanceTypeArgumentVariableUse(:final invocation):
            Selector selector = _elementMap.getSelector(invocation);
            if (rtiNeed.selectorNeedsTypeArguments(selector)) {
              return true;
            }
            break;
          case LocalTypeArgumentVariableUse(
            :final localFunction,
            :final invocation,
          ):
            // TODO(johnniwinther): We should be able to track direct local
            // function invocations and not have to use the selector here.
            Selector selector = _elementMap.getSelector(invocation);
            if (rtiNeed.localFunctionNeedsTypeArguments(localFunction) ||
                rtiNeed.selectorNeedsTypeArguments(selector)) {
              return true;
            }
            break;
          case MemberParameterVariableUse(:final member):
            if (_annotationsData
                .getParameterCheckPolicy(outermostEntity)
                .isEmitted) {
              return true;
            } else {
              FunctionEntity method = _elementMap.getMethod(
                member as ir.Procedure,
              );
              if (rtiNeed.methodNeedsSignature(method)) {
                return true;
              }
              if (rtiNeed.methodNeedsTypeArguments(method)) {
                // Stubs generated for this method might make use of this type
                // parameter for default type arguments.
                return true;
              }
            }
            break;
          case LocalParameterVariableUse(:final localFunction):
            if (_annotationsData
                .getParameterCheckPolicy(outermostEntity)
                .isEmitted) {
              return true;
            } else if (rtiNeed.localFunctionNeedsSignature(localFunction)) {
              return true;
            } else if (rtiNeed.localFunctionNeedsTypeArguments(localFunction)) {
              // Stubs generated for this local function might make use of this
              // type parameter for default type arguments.
              return true;
            }
            break;
          case MemberReturnTypeVariableUse(:final member):
            FunctionEntity method = _elementMap.getMethod(
              member as ir.Procedure,
            );
            if (rtiNeed.methodNeedsSignature(method)) {
              return true;
            }
            break;
          case LocalReturnTypeVariableUse(:final localFunction):
            if (localFunction.function.asyncMarker != ir.AsyncMarker.Sync) {
              // The Future/Iterator/Stream implementation requires the type.
              return true;
            }
            if (rtiNeed.localFunctionNeedsSignature(localFunction)) {
              return true;
            }
            break;
          case SimpleVariableUse.fieldType:
            if (_annotationsData
                .getParameterCheckPolicy(outermostEntity)
                .isEmitted) {
              return true;
            }
            break;
          case SimpleVariableUse.listLiteral:
            if (rtiNeed.classNeedsTypeArguments(
              _elementMap.commonElements.jsArrayClass,
            )) {
              return true;
            }
            break;
          case SimpleVariableUse.setLiteral:
            if (rtiNeed.classNeedsTypeArguments(
              _elementMap.commonElements.setLiteralClass,
            )) {
              return true;
            }
            break;
          case SimpleVariableUse.mapLiteral:
            if (rtiNeed.classNeedsTypeArguments(
              _elementMap.commonElements.mapLiteralClass,
            )) {
              return true;
            }
            break;
          case InstantiationTypeArgumentVariableUse(:final instantiation):
            // TODO(johnniwinther): Use the static type of the expression.
            if (rtiNeed.instantiationNeedsTypeArguments(
              null,
              instantiation.typeArguments.length,
            )) {
              return true;
            }
            break;
        }
      }
      return false;
    }

    if (includeForRti(scope.thisUsedAsFreeVariableIfNeedsRti)) {
      scope.thisUsedAsFreeVariable = true;
    }
    scope.freeVariablesForRti.forEach((
      TypeVariableTypeWithContext typeVariable,
      Set<VariableUse> useSet,
    ) {
      if (includeForRti(useSet)) {
        scope.freeVariables.add(typeVariable);
      }
    });
  }

  ClosureData createClosureEntities(
    JClosedWorldBuilder closedWorldBuilder,
    Map<MemberEntity, ClosureScopeModel> closureModels,
    ClosureRtiNeed rtiNeed,
    List<FunctionEntity> callMethods,
  ) {
    void processModel(MemberEntity member, ClosureScopeModel model) {
      Map<ir.VariableDeclaration, JContextField> allBoxedVariables = _elementMap
          .makeContextContainer(model.scopeInfo!, member);
      _scopeMap[member] = JsScopeInfo.from(
        allBoxedVariables,
        model.scopeInfo!,
        member.enclosingClass,
      );

      model.capturedScopesMap.forEach((
        ir.Node node,
        KernelCapturedScope scope,
      ) {
        Map<ir.VariableDeclaration, JContextField> boxedVariables = _elementMap
            .makeContextContainer(scope, member);
        _updateScopeBasedOnRtiNeed(scope, rtiNeed, member);

        if (scope is KernelCapturedLoopScope) {
          _capturedScopesMap[node as ir.TreeNode] = JsCapturedLoopScope.from(
            boxedVariables,
            scope,
            member.enclosingClass,
          );
        } else {
          _capturedScopesMap[node as ir.TreeNode] = JsCapturedScope.from(
            boxedVariables,
            scope,
            member.enclosingClass,
          );
        }
        allBoxedVariables.addAll(boxedVariables);
      });

      Map<ir.LocalFunction, KernelScopeInfo> closuresToGenerate =
          model.closuresToGenerate;
      for (ir.LocalFunction node in closuresToGenerate.keys) {
        ir.FunctionNode functionNode = node.function;
        JsClosureClassInfo closureClassInfo = _produceSyntheticElements(
          closedWorldBuilder,
          member,
          functionNode,
          closuresToGenerate[node]!,
          allBoxedVariables,
          rtiNeed,
          createSignatureMethod: rtiNeed.localFunctionNeedsSignature(
            functionNode.parent as ir.LocalFunction,
          ),
        );
        // Add also for the call method.
        _scopeMap[closureClassInfo.callMethod!] = closureClassInfo;
        if (closureClassInfo.signatureMethod != null) {
          _scopeMap[closureClassInfo.signatureMethod!] = closureClassInfo;

          // Set up capturedScope for signature method. This is distinct from
          // _capturedScopesMap because there is no corresponding ir.Node for
          // the signature.
          if (rtiNeed.localFunctionNeedsSignature(
                functionNode.parent as ir.LocalFunction,
              ) &&
              model.capturedScopesMap[functionNode] != null) {
            KernelCapturedScope capturedScope =
                model.capturedScopesMap[functionNode]!;
            assert(capturedScope is! KernelCapturedLoopScope);
            KernelCapturedScope signatureCapturedScope =
                KernelCapturedScope.forSignature(capturedScope);
            _updateScopeBasedOnRtiNeed(signatureCapturedScope, rtiNeed, member);
            _capturedScopeForSignatureMap[closureClassInfo
                .signatureMethod!] = JsCapturedScope.from(
              {},
              signatureCapturedScope,
              member.enclosingClass,
            );
          }
        }
        callMethods.add(closureClassInfo.callMethod!);
      }
    }

    closureModels.forEach((MemberEntity member, ClosureScopeModel model) {
      _reporter.withCurrentElement(member, () {
        processModel(member, model);
      });
    });
    return ClosureDataImpl(
      _elementMap,
      _scopeMap,
      _capturedScopesMap,
      _capturedScopeForSignatureMap,
      _localClosureRepresentationMap,
      _enclosingMembers,
    );
  }

  /// Given what variables are captured at each point, construct closure classes
  /// with fields containing the captured variables to replicate the Dart
  /// closure semantics in JS. If this closure captures any variables (meaning
  /// the closure accesses a variable that gets accessed at some point), then
  /// boxForCapturedVariables stores the local context for those variables.
  /// If no variables are captured, this parameter is null.
  JsClosureClassInfo _produceSyntheticElements(
    JClosedWorldBuilder closedWorldBuilder,
    MemberEntity member,
    ir.FunctionNode node,
    KernelScopeInfo info,
    Map<ir.VariableDeclaration, JContextField> boxedVariables,
    ClosureRtiNeed rtiNeed, {
    required bool createSignatureMethod,
  }) {
    _updateScopeBasedOnRtiNeed(info, rtiNeed, member);
    JsClosureClassInfo closureClassInfo = closedWorldBuilder.buildClosureClass(
      member,
      node,
      member.library as JLibrary,
      boxedVariables,
      info,
      createSignatureMethod: createSignatureMethod,
    );

    // We want the original declaration where that function is used to point
    // to the correct closure class.
    _enclosingMembers[closureClassInfo.callMethod!] = member;
    if (closureClassInfo.signatureMethod != null) {
      _enclosingMembers[closureClassInfo.signatureMethod!] = member;
    }
    if (node.parent is ir.Member) {
      assert(_elementMap.getMember(node.parent as ir.Member) == member);
    } else {
      assert(node.parent is ir.LocalFunction);
      _localClosureRepresentationMap[node.parent as ir.LocalFunction] =
          closureClassInfo;
    }
    return closureClassInfo;
  }
}

class JsScopeInfo extends ScopeInfo {
  /// Tag used for identifying serialized [JsScopeInfo] objects in a
  /// debugging data stream.
  static const String tag = 'scope-info';

  final Iterable<ir.VariableDeclaration> _localsUsedInTryOrSync;

  Set<Local>? _localsUsedInTryOrSyncCache;

  @override
  final Local? thisLocal;

  final Map<ir.VariableDeclaration, JContextField> _boxedVariables;

  Map<Local, JContextField>? _boxedVariablesCache;

  JsScopeInfo.internal(
    this._localsUsedInTryOrSync,
    this.thisLocal,
    this._boxedVariables,
  );

  JsScopeInfo.from(
    this._boxedVariables,
    KernelScopeInfo info,
    ClassEntity? enclosingClass,
  ) : thisLocal = info.hasThisLocal ? ThisLocal(enclosingClass!) : null,
      _localsUsedInTryOrSync = info.localsUsedInTryOrSync;

  void _ensureBoxedVariableCache(KernelToLocalsMap localsMap) {
    if (_boxedVariablesCache == null) {
      if (_boxedVariables.isEmpty) {
        _boxedVariablesCache = const {};
      } else {
        final cache = <Local, JContextField>{};
        _boxedVariables.forEach((
          ir.VariableDeclaration node,
          JContextField field,
        ) {
          cache[localsMap.getLocalVariable(node)] = field;
        });
        _boxedVariablesCache = cache;
      }
    }
  }

  @override
  void forEachBoxedVariable(
    KernelToLocalsMap localsMap,
    void Function(Local local, FieldEntity field) f,
  ) {
    _ensureBoxedVariableCache(localsMap);
    _boxedVariablesCache!.forEach(f);
  }

  @override
  bool localIsUsedInTryOrSync(KernelToLocalsMap localsMap, Local variable) {
    if (_localsUsedInTryOrSyncCache == null) {
      if (_localsUsedInTryOrSync.isEmpty) {
        _localsUsedInTryOrSyncCache = const {};
      } else {
        _localsUsedInTryOrSyncCache = {};
        for (ir.VariableDeclaration node in _localsUsedInTryOrSync) {
          _localsUsedInTryOrSyncCache!.add(localsMap.getLocalVariable(node));
        }
      }
    }
    return _localsUsedInTryOrSyncCache!.contains(variable);
  }

  @override
  String toString() {
    StringBuffer sb = StringBuffer();
    sb.write('this=$thisLocal,');
    sb.write('localsUsedInTryOrSync={${_localsUsedInTryOrSync.join(', ')}}');
    return sb.toString();
  }

  @override
  bool isBoxedVariable(KernelToLocalsMap localsMap, Local variable) {
    _ensureBoxedVariableCache(localsMap);
    return _boxedVariablesCache!.containsKey(variable);
  }

  factory JsScopeInfo.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    Iterable<ir.VariableDeclaration> localsUsedInTryOrSync =
        source.readTreeNodes<ir.VariableDeclaration>();
    Local? thisLocal = source.readLocalOrNull();
    Map<ir.VariableDeclaration, JContextField> boxedVariables = source
        .readTreeNodeMap<ir.VariableDeclaration, JContextField>(
          () => source.readMember() as JContextField,
        );
    source.end(tag);
    if (boxedVariables.isEmpty) boxedVariables = const {};
    return JsScopeInfo.internal(
      localsUsedInTryOrSync,
      thisLocal,
      boxedVariables,
    );
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(ScopeInfoKind.scopeInfo);
    sink.begin(tag);
    sink.writeTreeNodes(_localsUsedInTryOrSync);
    sink.writeLocalOrNull(thisLocal);
    sink.writeTreeNodeMap(_boxedVariables, sink.writeMember);
    sink.end(tag);
  }
}

class JsCapturedScope extends JsScopeInfo implements CapturedScope {
  /// Tag used for identifying serialized [JsCapturedScope] objects in a
  /// debugging data stream.
  static const String tag = 'captured-scope';

  @override
  final Local? contextBox;

  JsCapturedScope.internal(
    super.localsUsedInTryOrSync,
    super.thisLocal,
    super.boxedVariables,
    this.contextBox,
  ) : super.internal();

  JsCapturedScope.from(
    super.boxedVariables,
    super.capturedScope,
    super.enclosingClass,
  ) : contextBox =
          boxedVariables.isNotEmpty ? boxedVariables.values.first.box : null,
      super.from();

  @override
  bool get requiresContextBox => _boxedVariables.isNotEmpty;

  factory JsCapturedScope.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    Iterable<ir.VariableDeclaration> localsUsedInTryOrSync =
        source.readTreeNodes<ir.VariableDeclaration>();
    Local? thisLocal = source.readLocalOrNull();
    Map<ir.VariableDeclaration, JContextField> boxedVariables = source
        .readTreeNodeMap<ir.VariableDeclaration, JContextField>(
          () => source.readMember() as JContextField,
        );
    Local? context = source.readLocalOrNull();
    source.end(tag);
    return JsCapturedScope.internal(
      localsUsedInTryOrSync,
      thisLocal,
      boxedVariables,
      context,
    );
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(ScopeInfoKind.capturedScope);
    sink.begin(tag);
    sink.writeTreeNodes(_localsUsedInTryOrSync);
    sink.writeLocalOrNull(thisLocal);
    sink.writeTreeNodeMap(_boxedVariables, sink.writeMember);
    sink.writeLocalOrNull(contextBox);
    sink.end(tag);
  }
}

class JsCapturedLoopScope extends JsCapturedScope implements CapturedLoopScope {
  /// Tag used for identifying serialized [JsCapturedLoopScope] objects in a
  /// debugging data stream.
  static const String tag = 'captured-loop-scope';

  final List<ir.VariableDeclaration> _boxedLoopVariables;

  JsCapturedLoopScope.internal(
    super.localsUsedInTryOrSync,
    super.thisLocal,
    super.boxedVariables,
    super.context,
    this._boxedLoopVariables,
  ) : super.internal();

  JsCapturedLoopScope.from(
    super.boxedVariables,
    KernelCapturedLoopScope super.capturedScope,
    super.enclosingClass,
  ) : _boxedLoopVariables = capturedScope.boxedLoopVariables,
      super.from();

  @override
  bool get hasBoxedLoopVariables => _boxedLoopVariables.isNotEmpty;

  factory JsCapturedLoopScope.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    Iterable<ir.VariableDeclaration> localsUsedInTryOrSync =
        source.readTreeNodes<ir.VariableDeclaration>();
    Local? thisLocal = source.readLocalOrNull();
    Map<ir.VariableDeclaration, JContextField> boxedVariables = source
        .readTreeNodeMap<ir.VariableDeclaration, JContextField>(
          () => source.readMember() as JContextField,
        );
    Local? context = source.readLocalOrNull();
    List<ir.VariableDeclaration> boxedLoopVariables =
        source.readTreeNodes<ir.VariableDeclaration>();
    source.end(tag);
    return JsCapturedLoopScope.internal(
      localsUsedInTryOrSync,
      thisLocal,
      boxedVariables,
      context,
      boxedLoopVariables,
    );
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(ScopeInfoKind.capturedLoopScope);
    sink.begin(tag);
    sink.writeTreeNodes(_localsUsedInTryOrSync);
    sink.writeLocalOrNull(thisLocal);
    sink.writeTreeNodeMap(_boxedVariables, sink.writeMember);
    sink.writeLocalOrNull(contextBox);
    sink.writeTreeNodes(_boxedLoopVariables);
    sink.end(tag);
  }

  @override
  List<Local> getBoxedLoopVariables(KernelToLocalsMap localsMap) {
    List<Local> locals = [];
    for (ir.VariableDeclaration boxedLoopVariable in _boxedLoopVariables) {
      locals.add(localsMap.getLocalVariable(boxedLoopVariable));
    }
    return locals;
  }
}

// TODO(johnniwinther): Add unittest for the computed [ClosureClass].
class JsClosureClassInfo extends JsScopeInfo
    implements ClosureRepresentationInfo {
  /// Tag used for identifying serialized [JsClosureClassInfo] objects in a
  /// debugging data stream.
  static const String tag = 'closure-representation-info';

  @override
  JFunction? callMethod;

  @override
  JSignatureMethod? signatureMethod;

  /// The local used for this closure, if it is an anonymous closure, i.e.
  /// an `ir.FunctionExpression`.
  final Local? _closureEntity;

  /// The local variable that defines this closure, if it is a local function
  /// declaration.
  final ir.VariableDeclaration? _closureEntityVariable;

  @override
  final JClass closureClassEntity;

  final Map<ir.VariableDeclaration, JField> _variableToFieldMap;
  final Map<JTypeVariable, JField> _typeVariableToFieldMap;
  final Map<Local, JField> _localToFieldMap;
  Map<JField, Local>? _fieldToLocalsMap;

  JsClosureClassInfo.internal(
    super.localsUsedInTryOrSync,
    super.thisLocal,
    super.boxedVariables,
    this.callMethod,
    this.signatureMethod,
    this._closureEntity,
    this._closureEntityVariable,
    this.closureClassEntity,
    this._variableToFieldMap,
    this._typeVariableToFieldMap,
    this._localToFieldMap,
  ) : super.internal();

  JsClosureClassInfo.fromScopeInfo(
    this.closureClassEntity,
    ir.FunctionNode closureSourceNode,
    Map<ir.VariableDeclaration, JContextField> boxedVariables,
    KernelScopeInfo info,
    ClassEntity? enclosingClass,
    this._closureEntity,
    this._closureEntityVariable,
  ) : _variableToFieldMap = {},
      _typeVariableToFieldMap = {},
      _localToFieldMap = {},
      super.from(boxedVariables, info, enclosingClass);

  factory JsClosureClassInfo.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    Iterable<ir.VariableDeclaration> localsUsedInTryOrSync =
        source.readTreeNodes<ir.VariableDeclaration>();
    Local? thisLocal = source.readLocalOrNull();
    Map<ir.VariableDeclaration, JContextField> boxedVariables = source
        .readTreeNodeMap<ir.VariableDeclaration, JContextField>(
          () => source.readMember() as JContextField,
        );
    JFunction callMethod = source.readMember() as JFunction;
    JSignatureMethod? signatureMethod =
        source.readMemberOrNull() as JSignatureMethod?;
    Local? closureEntity = source.readLocalOrNull();
    ir.VariableDeclaration? closureEntityVariable =
        source.readTreeNodeOrNull() as ir.VariableDeclaration?;
    JClass closureClassEntity = source.readClass() as JClass;
    Map<ir.VariableDeclaration, JField> localToFieldMap = source
        .readTreeNodeMap<ir.VariableDeclaration, JField>(
          () => source.readMember() as JField,
        );
    Map<JTypeVariable, JField> typeVariableToFieldMap = source
        .readTypeVariableMap<JTypeVariable, JField>(
          () => source.readMember() as JField,
        );
    Map<Local, JField> thisLocalToFieldMap = source.readLocalMap(
      () => source.readMember() as JField,
    );
    source.end(tag);
    if (boxedVariables.isEmpty) boxedVariables = const {};
    if (localToFieldMap.isEmpty) localToFieldMap = const {};
    return JsClosureClassInfo.internal(
      localsUsedInTryOrSync,
      thisLocal,
      boxedVariables,
      callMethod,
      signatureMethod,
      closureEntity,
      closureEntityVariable,
      closureClassEntity,
      localToFieldMap,
      typeVariableToFieldMap,
      thisLocalToFieldMap,
    );
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(ScopeInfoKind.closureRepresentationInfo);
    sink.begin(tag);
    sink.writeTreeNodes(_localsUsedInTryOrSync);
    sink.writeLocalOrNull(thisLocal);
    sink.writeTreeNodeMap(_boxedVariables, sink.writeMember);
    sink.writeMember(callMethod!);
    sink.writeMemberOrNull(signatureMethod);
    sink.writeLocalOrNull(_closureEntity);
    sink.writeTreeNodeOrNull(_closureEntityVariable);
    sink.writeClass(closureClassEntity);
    sink.writeTreeNodeMap(_variableToFieldMap, sink.writeMember);
    sink.writeTypeVariableMap(_typeVariableToFieldMap, sink.writeMember);
    sink.writeLocalMap(_localToFieldMap, sink.writeMember);
    sink.end(tag);
  }

  bool hasFieldForLocal(Local local) => _localToFieldMap.containsKey(local);

  void registerFieldForLocal(Local local, JField field) {
    assert(_fieldToLocalsMap == null);
    _localToFieldMap[local] = field;
  }

  void registerFieldForVariable(ir.VariableDeclaration node, JField field) {
    assert(_fieldToLocalsMap == null);
    _variableToFieldMap[node] = field;
  }

  bool hasFieldForTypeVariable(JTypeVariable typeVariable) =>
      _typeVariableToFieldMap.containsKey(typeVariable);

  void registerFieldForTypeVariable(JTypeVariable typeVariable, JField field) {
    assert(_fieldToLocalsMap == null);
    _typeVariableToFieldMap[typeVariable] = field;
  }

  void registerFieldForBoxedVariable(
    ir.VariableDeclaration node,
    JField field,
  ) {
    assert(_boxedVariablesCache == null);
    _boxedVariables[node] = field as JContextField;
  }

  void _ensureFieldToLocalsMap(KernelToLocalsMap localsMap) {
    if (_fieldToLocalsMap == null) {
      _fieldToLocalsMap = {};
      _variableToFieldMap.forEach((ir.VariableDeclaration node, JField field) {
        _fieldToLocalsMap![field] = localsMap.getLocalVariable(node);
      });
      _typeVariableToFieldMap.forEach((
        TypeVariableEntity typeVariable,
        JField field,
      ) {
        _fieldToLocalsMap![field] = localsMap.getLocalTypeVariableEntity(
          typeVariable,
        );
      });
      _localToFieldMap.forEach((Local local, JField field) {
        _fieldToLocalsMap![field] = local;
      });
      if (_fieldToLocalsMap!.isEmpty) {
        _fieldToLocalsMap = const {};
      }
    }
  }

  @override
  Local getLocalForField(KernelToLocalsMap localsMap, FieldEntity field) {
    _ensureFieldToLocalsMap(localsMap);
    return _fieldToLocalsMap![field]!;
  }

  @override
  FieldEntity? get thisFieldEntity => _localToFieldMap[thisLocal];

  @override
  void forEachFreeVariable(
    KernelToLocalsMap localsMap,
    void Function(Local variable, JField field) f,
  ) {
    _ensureFieldToLocalsMap(localsMap);
    _ensureBoxedVariableCache(localsMap);
    _fieldToLocalsMap!.forEach((JField field, Local local) {
      f(local, field);
    });
    _boxedVariablesCache!.forEach(f);
  }

  @override
  bool get isClosure => true;

  @override
  Local? getClosureEntity(KernelToLocalsMap localsMap) {
    return _closureEntityVariable != null
        ? localsMap.getLocalVariable(_closureEntityVariable)
        : _closureEntity;
  }
}

abstract class ClosureRtiNeed {
  bool classNeedsTypeArguments(ClassEntity cls);

  bool methodNeedsTypeArguments(FunctionEntity method);

  bool methodNeedsSignature(MemberEntity method);

  bool localFunctionNeedsTypeArguments(ir.LocalFunction node);

  bool localFunctionNeedsSignature(ir.LocalFunction node);

  bool selectorNeedsTypeArguments(Selector selector);

  bool instantiationNeedsTypeArguments(
    FunctionType? functionType,
    int typeArgumentCount,
  );
}

/// A container for variables declared in a particular scope that are accessed
/// elsewhere.
// TODO(johnniwinther): Don't implement JClass. This isn't actually a class.
class JContext extends JClass {
  /// Tag used for identifying serialized [JContext] objects in a debugging data
  /// stream.
  static const String tag = 'context';

  JContext(LibraryEntity library, String name)
    : super(library as JLibrary, name, isAbstract: false);

  factory JContext.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    JLibrary library = source.readLibrary() as JLibrary;
    String name = source.readString();
    source.end(tag);
    return JContext(library, name);
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(JClassKind.context);
    sink.begin(tag);
    sink.writeLibrary(library);
    sink.writeString(name);
    sink.end(tag);
  }

  @override
  bool get isClosure => false;

  @override
  String toString() => '${jsElementPrefix}context($name)';
}

/// A variable that has been "boxed" to prevent name shadowing with the original
/// variable and ensure that this variable is updated/read with the most recent
/// value.
class JContextField extends JField {
  /// Tag used for identifying serialized [JContextField] objects in a debugging
  /// data stream.
  static const String tag = 'context-field';

  final BoxLocal box;

  JContextField(String name, this.box, {required bool isConst})
    : super(
        box.container.library as JLibrary,
        box.container as JClass,
        Name(name, box.container.library.canonicalUri),
        isStatic: false,
        isAssignable: true,
        isConst: isConst,
      );

  factory JContextField.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    String name = source.readString();
    final enclosingClass = source.readClass() as JClass;
    bool isConst = source.readBool();
    source.end(tag);
    return JContextField(name, BoxLocal(enclosingClass), isConst: isConst);
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(JMemberKind.contextField);
    sink.begin(tag);
    sink.writeString(name);
    sink.writeClass(enclosingClass!);
    sink.writeBool(isConst);
    sink.end(tag);
  }

  // TODO(johnniwinther): Remove these anomalies. Maybe by separating the
  // J-entities from the K-entities.
  @override
  bool get isInstanceMember => false;

  @override
  bool get isTopLevel => false;

  @override
  bool get isStatic => false;
}

class JClosureClass extends JClass {
  /// Tag used for identifying serialized [JClosureClass] objects in a
  /// debugging data stream.
  static const String tag = 'closure-class';

  JClosureClass(super.library, super.name) : super(isAbstract: false);

  factory JClosureClass.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    JLibrary library = source.readLibrary() as JLibrary;
    String name = source.readString();
    source.end(tag);
    return JClosureClass(library, name);
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(JClassKind.closure);
    sink.begin(tag);
    sink.writeLibrary(library);
    sink.writeString(name);
    sink.end(tag);
  }

  @override
  bool get isClosure => true;

  @override
  String toString() => '${jsElementPrefix}closure_class($name)';
}

class AnonymousClosureLocal implements Local {
  final JClosureClass closureClass;

  AnonymousClosureLocal(this.closureClass);

  @override
  String get name => '';

  @override
  int get hashCode => closureClass.hashCode * 13;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    if (other is! AnonymousClosureLocal) return false;
    return closureClass == other.closureClass;
  }

  @override
  String toString() =>
      '${jsElementPrefix}anonymous_closure_local(${closureClass.name})';
}

class JClosureField extends JField implements PrivatelyNamedJSEntity {
  /// Tag used for identifying serialized [JClosureClass] objects in a
  /// debugging data stream.
  static const String tag = 'closure-field';

  @override
  final String declaredName;

  JClosureField(
    String name,
    JsClosureClassInfo containingClass,
    String declaredName, {
    required bool isConst,
    required bool isAssignable,
  }) : this.internal(
         containingClass.closureClassEntity.library,
         containingClass.closureClassEntity as JClosureClass,
         Name(name, containingClass.closureClassEntity.library.canonicalUri),
         declaredName,
         isAssignable: isAssignable,
         isConst: isConst,
       );

  JClosureField.internal(
    super.library,
    JClosureClass super.enclosingClass,
    super.memberName,
    this.declaredName, {
    required super.isConst,
    required super.isAssignable,
  }) : super(isStatic: false);

  factory JClosureField.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    final cls = source.readClass() as JClosureClass;
    String name = source.readString();
    String declaredName = source.readString();
    bool isConst = source.readBool();
    bool isAssignable = source.readBool();
    source.end(tag);
    return JClosureField.internal(
      cls.library,
      cls,
      Name(name, cls.library.canonicalUri),
      declaredName,
      isAssignable: isAssignable,
      isConst: isConst,
    );
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(JMemberKind.closureField);
    sink.begin(tag);
    sink.writeClass(enclosingClass!);
    sink.writeString(name);
    sink.writeString(declaredName);
    sink.writeBool(isConst);
    sink.writeBool(isAssignable);
    sink.end(tag);
  }

  @override
  Entity get rootOfScope => enclosingClass!;
}

class ContextClassData implements JClassData {
  /// Tag used for identifying serialized [ContextClassData] objects in a
  /// debugging data stream.
  static const String tag = 'context-class-data';

  @override
  final ClassDefinition definition;

  @override
  final InterfaceType? thisType;

  @override
  final OrderedTypeSet orderedTypeSet;

  @override
  final InterfaceType? supertype;

  ContextClassData(
    this.definition,
    this.thisType,
    this.supertype,
    this.orderedTypeSet,
  );

  factory ContextClassData.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    ClassDefinition definition = ClassDefinition.readFromDataSource(source);
    InterfaceType thisType = source.readDartType() as InterfaceType;
    InterfaceType supertype = source.readDartType() as InterfaceType;
    OrderedTypeSet orderedTypeSet = OrderedTypeSet.readFromDataSource(source);
    source.end(tag);
    return ContextClassData(definition, thisType, supertype, orderedTypeSet);
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(JClassDataKind.context);
    sink.begin(tag);
    definition.writeToDataSink(sink);
    sink.writeDartType(thisType!);
    sink.writeDartType(supertype!);
    orderedTypeSet.writeToDataSink(sink);
    sink.end(tag);
  }

  @override
  bool get isMixinApplication => false;

  @override
  bool get isEnumClass => false;

  @override
  FunctionType? get callType => null;

  @override
  List<InterfaceType> get interfaces => const <InterfaceType>[];

  @override
  InterfaceType? get mixedInType => null;

  @override
  InterfaceType? get jsInteropType => thisType;

  @override
  InterfaceType? get rawType => thisType;

  @override
  InterfaceType? get instantiationToBounds => thisType;

  @override
  List<Variance> getVariances() => [];
}

class ClosureClassData extends ContextClassData {
  /// Tag used for identifying serialized [ClosureClassData] objects in a
  /// debugging data stream.
  static const String tag = 'closure-class-data';

  @override
  FunctionType? callType;

  ClosureClassData(
    super.definition,
    super.thisType,
    super.supertype,
    super.orderedTypeSet,
  );

  factory ClosureClassData.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    ClassDefinition definition = ClassDefinition.readFromDataSource(source);
    InterfaceType thisType = source.readDartType() as InterfaceType;
    InterfaceType supertype = source.readDartType() as InterfaceType;
    OrderedTypeSet orderedTypeSet = OrderedTypeSet.readFromDataSource(source);
    FunctionType callType = source.readDartType() as FunctionType;
    source.end(tag);
    return ClosureClassData(definition, thisType, supertype, orderedTypeSet)
      ..callType = callType;
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(JClassDataKind.closure);
    sink.begin(tag);
    definition.writeToDataSink(sink);
    sink.writeDartType(thisType!);
    sink.writeDartType(supertype!);
    orderedTypeSet.writeToDataSink(sink);
    sink.writeDartType(callType!);
    sink.end(tag);
  }
}

abstract class ClosureMemberData implements JMemberData {
  @override
  final MemberDefinition definition;
  final InterfaceType? memberThisType;

  ClosureMemberData(this.definition, this.memberThisType);

  @override
  InterfaceType? getMemberThisType(covariant JsToElementMap elementMap) {
    return memberThisType;
  }
}

class ClosureFunctionData extends ClosureMemberData
    with FunctionDataTypeVariablesMixin, FunctionDataForEachParameterMixin
    implements FunctionData {
  /// Tag used for identifying serialized [ClosureFunctionData] objects in a
  /// debugging data stream.
  static const String tag = 'closure-function-data';

  final FunctionType functionType;
  @override
  ir.FunctionNode get functionNode => _functionNode.loaded();
  final Deferrable<ir.FunctionNode> _functionNode;
  @override
  final ClassTypeVariableAccess classTypeVariableAccess;

  ClosureFunctionData(
    super.definition,
    super.memberThisType,
    this.functionType,
    ir.FunctionNode functionNode,
    this.classTypeVariableAccess,
  ) : _functionNode = Deferrable.eager(functionNode);

  ClosureFunctionData._deserialized(
    super.definition,
    super.memberThisType,
    this.functionType,
    this._functionNode,
    this.classTypeVariableAccess,
  );

  static ir.FunctionNode _readFunctionNode(DataSourceReader source) {
    return source.readTreeNode() as ir.FunctionNode;
  }

  factory ClosureFunctionData.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    ClosureMemberDefinition definition =
        MemberDefinition.readFromDataSource(source) as ClosureMemberDefinition;
    InterfaceType? memberThisType =
        source.readDartTypeOrNull() as InterfaceType?;
    FunctionType functionType = source.readDartType() as FunctionType;
    Deferrable<ir.FunctionNode> functionNode = source.readDeferrable(
      _readFunctionNode,
    );
    ClassTypeVariableAccess classTypeVariableAccess = source.readEnum(
      ClassTypeVariableAccess.values,
    );
    source.end(tag);
    return ClosureFunctionData._deserialized(
      definition,
      memberThisType,
      functionType,
      functionNode,
      classTypeVariableAccess,
    );
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(JMemberDataKind.closureFunction);
    sink.begin(tag);
    definition.writeToDataSink(sink);
    sink.writeDartTypeOrNull(memberThisType);
    sink.writeDartType(functionType);
    sink.writeDeferrable(() => sink.writeTreeNode(functionNode));
    sink.writeEnum(classTypeVariableAccess);
    sink.end(tag);
  }

  @override
  FunctionType getFunctionType(IrToElementMap elementMap) {
    return functionType;
  }
}

class ClosureFieldData extends ClosureMemberData implements JFieldData {
  /// Tag used for identifying serialized [ClosureFieldData] objects in a
  /// debugging data stream.
  static const String tag = 'closure-field-data';

  DartType? _type;

  ClosureFieldData(super.definition, super.memberThisType);

  factory ClosureFieldData.readFromDataSource(DataSourceReader source) {
    source.begin(tag);
    MemberDefinition definition = MemberDefinition.readFromDataSource(source);
    InterfaceType? memberThisType =
        source.readDartTypeOrNull() as InterfaceType?;
    source.end(tag);
    return ClosureFieldData(definition, memberThisType);
  }

  @override
  void writeToDataSink(DataSinkWriter sink) {
    sink.writeEnum(JMemberDataKind.closureField);
    sink.begin(tag);
    definition.writeToDataSink(sink);
    sink.writeDartTypeOrNull(memberThisType);
    sink.end(tag);
  }

  @override
  DartType getFieldType(IrToElementMap elementMap) {
    if (_type != null) return _type!;
    ir.Node sourceNode = definition.node;
    ir.DartType type;
    if (sourceNode is ir.Class) {
      type = sourceNode.getThisType(
        elementMap.coreTypes,
        sourceNode.enclosingLibrary.nonNullable,
      );
    } else if (sourceNode is ir.VariableDeclaration) {
      type = sourceNode.type;
    } else if (sourceNode is ir.Field) {
      type = sourceNode.type;
    } else if (sourceNode is ir.TypeLiteral) {
      type = sourceNode.type;
    } else if (sourceNode is ir.Typedef) {
      type = sourceNode.type!;
    } else if (sourceNode is ir.TypeParameter) {
      type = sourceNode.bound;
    } else {
      failedAt(
        definition.location,
        'Unexpected node type $sourceNode in '
        'ClosureFieldData.getFieldType',
      );
    }
    return _type = elementMap.getDartType(type);
  }

  @override
  bool get isCovariantByDeclaration => false;

  @override
  ClassTypeVariableAccess get classTypeVariableAccess =>
      ClassTypeVariableAccess.none;
}
